/**
 * Triple-B micro Framework for PHP5
 * Copyright (C) 2013  PT. Triple-B Shamany
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package ufwk;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import javax.sql.DataSource;

/**
 * Wrapper standar JDBC yang akan digunakan langsung oleh PHP script.
 * Kelas ini Thread-safe karena instance akan dibuat untuk setiap
 * eksekusi servlet (Quercus) yang menjalankan script.
 *
 * @author  Badrus Said <badrus.said@gmail.com>
 */
public final class JDBC {
    /* Cache lookup (initial context) */
    private static final Map<String, DataSource> jmap = new HashMap<String, DataSource>(8);

    /* java.sql.Connection instance */
    private final Connection conn;

    /* java.sql.DatabaseMetaData instance */
    private final DatabaseMetaData meta;

    /**
     * Naturally, eksekusi pada PHP adalah sekuensial, tanpa Thread.
     * Penggunaan single Statement untuk seluruh eksekusi seharusnya cukup "aman".
     */
    private final Statement stmt;

    /**
     * List ResultSet yang dihasilkan oleh statement selama eksekusi script
     */
    private final List<ResultSet> rset = new ArrayList<ResultSet>();

    public JDBC(String lookup) throws SQLException
    {
        DataSource datasource = null;
        if (jmap.containsKey(lookup)) datasource = jmap.get(lookup);
        else {
            try {
                Context ic = new InitialContext();
                datasource = (DataSource) ic.lookup(lookup);
                jmap.put(lookup, datasource);
            } catch(NamingException ne) {
                throw new SQLException(ne.getMessage());
            }
        }
        if (datasource == null) {
            throw new SQLException(String.format("UnknownDataSourceException: %s", lookup));
        } else {
            conn = datasource.getConnection();
            stmt = conn.createStatement();
            meta = conn.getMetaData();
        }
    }

    /**
     * Tutup semua resource yang ada: Connection, Statement dan ResultSet
     */
    public void close()
    {
        if (!rset.isEmpty()) {
            for(int i = 0, j = rset.size(); i < j; i++) {
                try { rset.get(i).close(); } catch(SQLException sqle) {}
            }
        }
        if (stmt != null) {
            try { stmt.close(); } catch(SQLException sqle) {}
        }
        try {
            conn.close();
        } catch(SQLException sqle) {}
    }

    /**
     * Notify driver (jika mendukung transaksi) untuk mark transaksi
     */
    public void beginTransaction() throws SQLException
    {
        if (meta.supportsTransactions()) conn.setAutoCommit(false);
    }

    /**
     * Notify driver (jika mendukung transaksi) untuk commit transaksi
     */
    public void commit() throws SQLException
    {
        if (meta.supportsTransactions() && !conn.getAutoCommit()) {
            conn.commit();
            conn.setAutoCommit(true);
        }
    }

    /**
     * Notify driver (jika mendukung transaksi) untuk rollback transaksi
     */
    public void rollback() throws SQLException
    {
        if (meta.supportsTransactions() && !conn.getAutoCommit()) {
            conn.rollback();
            conn.setAutoCommit(true);
        }
    }

    /**
     * Implementasi mysql_real_escape_string pada native PHP
     */
    public String escape(String text)
    {
        StringBuilder sb = new StringBuilder();
        char c;
        for(int i = 0, j = text.length(); i < j; i++) {
            c = text.charAt(i);
            switch(c) {
                case '\u0000': sb.append('\\').append(c); break;
                case '\n': sb.append('\\').append('n'); break;
                case '\r': sb.append('\\').append('r'); break;
                case '\\': sb.append('\\').append('\\'); break;
                case '\'': sb.append('\\').append('\''); break;
                case '"': sb.append('\\').append('\"'); break;
                case '\032': sb.append('\\').append('Z'); break;
                default: sb.append(c);
            }
        }

        return sb.toString();
    }

    /**
     * Salah satu alasan tidak menggunakan implementasi PDO Quercus adalah
     * framework membutuhkan attribute nama produk JDBC untuk membentuk
     * SQL pada kelas Statement (script)
     */
    public String getProductName()
        throws SQLException
    {
        return meta.getDatabaseProductName().toLowerCase();
    }

    /**
     * Eksekusi SQL statement (SELECT)
     */
    public ResultSet executeQuery(String statement)
        throws SQLException
    {
        ResultSet rs = stmt.executeQuery(statement);
        rset.add(rs);
        return rs;
    }

    /**
     * Eksekusi SQL statement (INSERT/UPDATE/DELETE)
     *
     * Khusus untuk statement INSERT, pada kelas Statement (script), return
     * adalah last_insert_id (jika ada).
     */
    public int executeUpdate(String statement, boolean getGeneratedKeys)
        throws SQLException
    {
        int gkey = -1;
        if (getGeneratedKeys) {
            gkey = stmt.executeUpdate(statement, Statement.RETURN_GENERATED_KEYS);
            if (gkey > 0) {
                ResultSet rset = stmt.getGeneratedKeys();
                if (rset.next()) {
                    gkey = rset.getInt(1);
                }
                rset.close();
            }
        } else {
            gkey = stmt.executeUpdate(statement);
        }
        return gkey;
    }
    public int executeUpdate(String statement)
        throws SQLException
    {
        return executeUpdate(statement, false);
    }
}
